from typing import List, Tuple, Callable, Any, Union

from ftplib import FTP
from ftplib import error_temp
from script.util.Env import Env

import os


class FtpServer(object):
    class ProgressCallback(object):
        def __init__(self, cb: Callable[[int, ], Any] = None):
            self.callback = cb
            self.progress = 0

        def on_progress(self, progress: int):
            self.progress += progress
            if self.callback:
                self.callback(self.progress)

    def __init__(self, name: str, host: str, username: str, password: str):
        self.mName = name
        self.mHost = host
        self.mUsername = username
        self.mPassword = password

        self.mServer = FTP()
        self.mServer.connect(self.mHost)
        self.mServer.login(self.mUsername, self.mPassword)
        self.mPwd = '~'

    def ftp(self, restore_path: bool = True) -> FTP:
        try:
            self.mServer.voidcmd('NOOP')  # test whether connection available
        except error_temp:
            self.mServer.connect(self.mHost)
            self.mServer.login(self.mUsername, self.mPassword)
            if restore_path:
                self.mServer.cwd(self.mPwd)

        return self.mServer

    def name(self) -> str:
        return self.mName

    def cd(self, path: str) -> bool:
        self.mPwd = path
        self.ftp(False).cwd(path)

        return True

    def mkd(self, path: str) -> bool:
        ftp = self.ftp()
        ftp.cwd('~')
        base_path = ftp.pwd()
        if not path.startswith('/'):
            path = os.path.join(base_path, path)
        else:
            ftp.cwd('/')

        path = path.lstrip(base_path)
        path = path.strip('/')
        seg = path.split('/')

        for filename in seg:
            try:
                ftp.cwd(filename)
            except Exception:
                ftp.mkd(filename)
                ftp.cwd(filename)
        return True

    def ls(self, path: str = '.') -> List[str]:
        return self.ftp().nlst(path)

    def ls_info(self, path: str = '.') -> List[Tuple[str, bool, int]]:
        lines = []

        def cb(line):
            nonlocal lines
            lines.append(line)

        self.ftp().dir(path, cb)

        lines = [line.split() for line in lines]
        lines = [(line[-1], line[0][0] == 'd', int(line[-5])) for line in lines]
        return lines

    def dir(self, path: str = '.'):
        lines = []

        def cb(line):
            nonlocal lines
            lines.append(line)
        self.ftp().dir(path, cb)
        return lines

    def pwd(self) -> str:
        self.mPwd = self.ftp().pwd()
        return self.mPwd

    def delete(self, path: str):
        self.ftp().delete(path)

    def rmdir(self, path: str):
        ftp = self.ftp()
        info_list = self.ls_info(path)
        for info in info_list:
            p = '%s/%s' % (path, info[0])
            if info[1]:
                self.rmdir(p)
                ftp.rmd(p)
            else:
                self.delete(p)

    def upload(self, local: str, remote: str, cb: Union[None, ProgressCallback, Callable[[int, ], Any]] = None) -> bool:
        # prepare callback
        progress_cb: Union[None, FtpServer.ProgressCallback] = None
        if cb is None:
            progress_cb = None
        elif isinstance(cb, FtpServer.ProgressCallback):
            progress_cb = cb
        else:
            progress_cb = FtpServer.ProgressCallback(cb)

        ###############
        ftp = self.ftp()
        if os.path.isdir(local):  # download dir
            result = True
            if remote not in ftp.nlst():
                ftp.mkd(remote)
            ftp.cwd(remote)
            for filename in os.listdir(local):
                result = self.upload('%s/%s' % (local, filename), filename, progress_cb)
                if not result:
                    break
            ftp.cwd('..')

            return result
        else:  # download file
            result = True
            size = os.path.getsize(local)

            # try to find the file on ftp server
            found = None
            ftp_file_info = self.ls_info()
            for file_info in ftp_file_info:
                if remote == file_info[0]:
                    found = file_info
                    break

            # upload if file not uploaded
            try:
                if found is None or size != found[2]:  # file not exists or should be overwritten
                    with open(local, 'rb') as file:
                        if progress_cb:
                            ftp.storbinary('STOR %s' % remote, file, 1024,
                                           lambda data: progress_cb.on_progress(len(data)))
                        else:
                            ftp.storbinary('STOR %s' % remote, file, 1024)
                else:  # file already exists
                    # update progress only
                    if progress_cb:
                        progress_cb.on_progress(size)
            except ConnectionResetError:
                result = False

            return result

    def set_synced(self):
        ftp = self.ftp()
        synced_file = '%s/synced' % Env.root_dir()
        with open(synced_file, 'rb') as file:
            ftp.storbinary('STOR synced', file, 1024)

    def quit(self):
        try:
            self.mServer.quit()
        except error_temp:
            pass  # ignore this error
